﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents a connector of two durational symbols.
    /// </summary>
    public abstract class Connector : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Connector"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this connector.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that directly owns this connector.
        /// </para>
        /// </param>
        /// <param name="presentedConnector">
        /// <para>
        /// Type: <see cref="ConnectorElement"/>
        /// </para>
        /// <para>
        /// The presented connector element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="owner"/> does not contain any of the symbols connected by the <paramref name="presentedConnector"/>.
        /// </exception>
        protected Connector(XmlPackagePart packagePart, System owner, ConnectorElement presentedConnector)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (presentedConnector == null)
                throw new ArgumentNullException("presentedConnector");

            var startPart = presentedConnector.Start.FindOwner<PartElement>().Descriptor;
            var endPart = presentedConnector.End.FindOwner<PartElement>().Descriptor;

            start = (
                from staff in owner.Staves
                where staff.Descriptor.FindOwner<PartDescriptorElement>() == startPart && staff.FindSymbolFor(presentedConnector.Start) != null
                select staff.FindSymbolFor(presentedConnector.Start)
                ).SingleOrDefault();
            end = (
                from staff in owner.Staves
                where staff.Descriptor.FindOwner<PartDescriptorElement>() == endPart && staff.FindSymbolFor(presentedConnector.End) != null
                select staff.FindSymbolFor(presentedConnector.End)
                ).SingleOrDefault();

            if (start == null && end == null)
                throw new ArgumentException("At least one of the symbols connected by the presented connector must be presented on the owner system.");

            if (start != null)
                startAttribute.Value = start.ID.ToString();
            else
                startAttribute.Value = "0";
            if (end != null)
                endAttribute.Value = end.ID.ToString();
            else
                endAttribute.Value = "0";

            this.presentedConnector = presentedConnector;
            presentedConnectorAttribute.Value = presentedConnector.ID.ToString();

            startingPoint.SetOwner(startingPointElement);
            endingPoint.SetOwner(endingPointElement);

            typeAttribute.Value = GetType().FullName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Connector"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this connector.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that directly owns this connector.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the serialized data of this connector.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Connector(XmlPackagePart packagePart, System owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            presentedConnectorAttribute = element.Attributes[PackageVocabulary.IDRefAttribute];

            startAttribute = element.Attributes[PackageVocabulary.StartAttribute];
            startingPointElement = element[PackageVocabulary.StartingPointElementName, PackageVocabulary.PackageNamespaceUri];
            startingPoint = new Point(startingPointElement.FirstChild as XmlElement);

            endAttribute = element.Attributes[PackageVocabulary.EndAttribute];
            endingPointElement = element[PackageVocabulary.EndingPointElementName, PackageVocabulary.PackageNamespaceUri];
            endingPoint = new Point(endingPointElement.FirstChild as XmlElement);

            typeAttribute = element.Attributes[PackageVocabulary.TypeAttribute];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = CreateConnectorElement(document);

            startAttribute = document.CreateAttribute(PackageVocabulary.StartAttribute);
            endAttribute = document.CreateAttribute(PackageVocabulary.EndAttribute);
            presentedConnectorAttribute = document.CreateAttribute(PackageVocabulary.IDRefAttribute);
            typeAttribute = document.CreateAttribute(PackageVocabulary.TypeAttribute);

            startingPointElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.StartingPointElementName,
                PackageVocabulary.PackageNamespaceUri
                );
            endingPointElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.EndingPointElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            element.Attributes.Append(startAttribute);
            element.Attributes.Append(endAttribute);
            element.Attributes.Append(presentedConnectorAttribute);
            element.Attributes.Append(typeAttribute);

            element.AppendChild(startingPointElement);
            element.AppendChild(endingPointElement);

            return element;
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// When overriden in a derived class, this method creates the XML element
        /// that will hold the data of this connector.
        /// </summary>
        /// <remarks>
        /// The attributes and child elements for inherited properites as well as the type information
        /// will be added by the base class.
        /// </remarks>
        /// <param name="document">
        /// <para>
        /// Type: <see cref="XmlDocument"/>
        /// </para>
        /// <para>
        /// The document that will own the created element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The created element.
        /// </para>
        /// </returns>
        protected abstract XmlElement CreateConnectorElement(XmlDocument document);

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates the representation of a given connector element.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that will own the created connector.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that will directly own the created connector.
        /// </para>
        /// </param>
        /// <param name="presentedElement">
        /// <para>
        /// Type: <see cref="ConnectorElement"/>
        /// </para>
        /// <para>
        /// The presented element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Connector"/>
        /// </para>
        /// <para>
        /// The created connector.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="presentedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="presentedElement"/> has an unknown type.
        /// </exception>
        public static Connector CreateRepresentation(XmlPackagePart packagePart, System owner, ConnectorElement presentedElement)
        {
            if (presentedElement == null)
                throw new ArgumentNullException("presentedElement");

            if (presentedElement is LegatoElement)
                return new Slur(packagePart, owner, presentedElement as LegatoElement);
            else if (presentedElement is TieElement)
                return new Tie(packagePart, owner, presentedElement as TieElement);

            throw new ArgumentException("Unknown connector type!", "presentedElement");
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the durational symbol at which the connector ends.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The durational symbol at which the connector ends.<br/>
        /// This value may be <see langword="null"/> if the symbol
        /// is not a part of the owner system. Such a case is treated
        /// as if the connector is continued in the next system and
        /// the <see cref="EndingPoint"/> is placed near the end of
        /// the owner system.
        /// </para>
        /// </value>
        public DurationalSymbol End
        {
            get
            {
                if (end == null && endAttribute.Value != "0")
                    end = PackagePart.Package.IDMap.FindObject<DurationalSymbol>(new Guid(endAttribute.Value));

                return end;
            }
        }

        /// <summary>
        /// Gets or sets the point at which the connector ends.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates of the point at which the connector ends.
        /// The values are in centimeters relative to the top-left corner
        /// of the owner system.
        /// </para>
        /// </value>
        public Point EndingPoint
        {
            get { return new Point(endingPoint); }
            set
            {
                endingPoint.SetOwner(null);
                endingPoint = value;
                endingPoint.SetOwner(endingPointElement);

                RaisePropertyChanged("EndingPoint");
            }
        }

        /// <summary>
        /// Gets the connector element presented by this connector.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ConnectorElement"/>
        /// </para>
        /// <para>
        /// The connector element presented by this connector.
        /// </para>
        /// </value>
        public ConnectorElement PresentedConnector
        {
            get
            {
                if (presentedConnector == null)
                    presentedConnector = PackagePart.Package.IDMap.FindObject<ConnectorElement>(new Guid(presentedConnectorAttribute.Value));

                return presentedConnector;
            }
        }

        /// <summary>
        /// Gets the durational symbol at which the connector starts.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The durational symbol at which the connector starts.<br/>
        /// This value may be <see langword="null"/> if the symbol
        /// is not a part of the owner system. Such a case is treated
        /// as if the connector is continued from the previous system and
        /// the <see cref="StartingPoint"/> is placed near the beginning of
        /// the owner system.
        /// </para>
        /// </value>
        public DurationalSymbol Start
        {
            get 
            {
                if (start == null && startAttribute.Value != "0")
                    start = PackagePart.Package.IDMap.FindObject<DurationalSymbol>(new Guid(startAttribute.Value));
                
                return start; 
            }
        }

        /// <summary>
        /// Gets or sets the point at which the connector starts.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Point"/>
        /// </para>
        /// <para>
        /// The coordinates of the point at which the connector starts.
        /// The values are in centimeters relative to the top-left corner
        /// of the owner system.
        /// </para>
        /// </value>
        public Point StartingPoint
        {
            get { return new Point(startingPoint); }
            set
            {
                startingPoint.SetOwner(null);
                startingPoint = value;
                startingPoint.SetOwner(startingPointElement);

                RaisePropertyChanged("StartingPoint");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="End"/> property.
        /// </summary>
        private DurationalSymbol end;
        /// <summary>
        /// Holds the value of the <see cref="End"/> property.
        /// </summary>
        private XmlAttribute endAttribute;
        /// <summary>
        /// Holds the value of the <see cref="EndingPoint"/> property.
        /// </summary>
        private Point endingPoint;
        /// <summary>
        /// Holds the value of the <see cref="EndingPoint"/> property.
        /// </summary>
        private XmlElement endingPointElement;
        /// <summary>
        /// Holds a reference to the presented connector element.
        /// </summary>
        private ConnectorElement presentedConnector;
        /// <summary>
        /// Holds the identifier of the presented connector element.
        /// </summary>
        private XmlAttribute presentedConnectorAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Start"/> property.
        /// </summary>
        private DurationalSymbol start;
        /// <summary>
        /// Holds the value of the <see cref="Start"/> property.
        /// </summary>
        private XmlAttribute startAttribute;
        /// <summary>
        /// Holds the value of the <see cref="StartingPoint"/> property.
        /// </summary>
        private Point startingPoint;
        /// <summary>
        /// Holds the value of the <see cref="StartingPoint"/> property.
        /// </summary>
        private XmlElement startingPointElement;
        /// <summary>
        /// Holds the type of the connector class.
        /// </summary>
        private XmlAttribute typeAttribute;

        #endregion
    }
}
